socket = 套接字

socket: 实现软件或程序之间的通信

在进行网络传输或通信的时候只能传递通过字符串转码后的bytes类型,然后再对bytes类型进行解码获取传递的数据

1.socket层


2.套接字的两个种族

  • 基于文件类型的套接字家族 -> 现在几乎不用
    • 套接字家族的名称: AF_UNX
    • UNX一切皆文件,以文件作为介质进行传输

  • 基于网络类型的套接字家族 -> 现在常用
    • 套接字家族的名称: AF_INET
    • AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET

3.基于TCP协议的socket


  • 先启动服务端文件再启动客户端文件

  • 客户端文件无需执行 conn, addr = sk.accept() 直接使用 sk 对象进行消息的收发

  • 有收必有发,收发必相等 -> 如果客户端执行了 send 服务端必须也执行 recv 反之,因为如果客户端或服务端的 conn.recv(1024) 没有接收到消息就会进行阻塞(阻塞: 一直等待,无法运行下面的代码,直到接收到消息)

  • sk.listen(num) -> 监听链接,如果listen传入一个数值,那么就代表只允许多少个客户端进行连接,如果不传则就没有限制,python3.2一下的一定要传

  • recv(num) 只会接受num个字节内的内容,但是num最好不要超过4096

  • 不支持多个客户端和服务端进行同时通讯

  • 在 Pycharm 下切换文件查看打印内容


# server.py 服务端

import socket

sk = socket.socket()  # 创建socket -> 买手机
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 避免服务器重启时报address already in use, 因为服务重启的时候电脑不会立刻关闭端口,而是过一会才关闭,此时端口就会被占用,也有可能是超过了网络最大链接数
# sk.bind(('ip地址', 端口))
sk.bind(('127.0.0.1', 8080))  # 把地址绑定到socket,传入的值必须是元组 -> 绑定手机卡
sk.listen()  # TCP开始监听链接,如果listen传入一个数值,那么就代表只允许多少个客户端进行连接,如果不传则就没有限制,python3.2一下的一定要传 -> 开始监听有没有人给你打电话
conn, addr = sk.accept()  # 等待接收客户端的链接 返回值: 链接, 地址 -> 等待别人给你打电话,并且接听别人的电话

# ——————————————————————————————————

ret = conn.recv(1024)  # 接收客户端发送过来的消息(返回值: bytes类型),如果没有接收到就会进行阻塞(阻塞: 一直等待,无法运行下面的代码,直到接收到消息)只会接收1024个字节内的内容,如果超过了1024个字节那么再次 conn.recv 就可以获取多余的内容 -> 听别人说话
print(ret.decode('utf-8'))  # 打印客户端发送过来的消息
conn.send(bytes('客户端,你好', encoding='utf-8'))  # 向客户端发送消息(发送的消息必须是bytes类型)-> 和别人说话
ret = conn.recv(1024).decode('utf-8')  # 可以在 recv 后面直接进行解码 -> 听别人说话
print(ret)  # 打印客户端发送过来的消息

# ——————————————————————————————————

conn.close()  # 关闭客户端的socket -> 挂电话
sk.close()  # 关闭服务器的socket(可选) -> 关手机

# client.py 客户端

import socket

sk = socket.socket()  # 创建客户端socket -> 买手机
sk.connect(('127.0.0.1', 8080))  # 尝试链接服务端,传入的值必须是元组 -> 拨打别人的电话

# ——————————————————————————————————

sk.send(bytes('服务端,你好', encoding='utf-8'))  # 向客户端发送消息 -> 和别人说话
ret = sk.recv(1024).decode('utf-8')  # 接收服务端发送过来的消息(返回值: bytes类型)如果没有接收到就会进行阻塞(阻塞: 一直等待,无法运行下面的代码,直到接收到消息)只会接收1024个字节内的内容,如果超过了1024个字节那么再次 conn.recv 就可以获取多余的内容,可以在 recv 后面直接进行解码 -> 听别人说话
print(ret)  # 打印服务端发送过来的消息
sk.send(bytes('服务端,再见', encoding='utf-8'))  # 向客户端发送消息 -> 和别人说话

# ——————————————————————————————————

sk.close()  # 关闭客户端的stroke -> 挂电话

  • 小型的聊天功能

# server.py 服务端

import socket

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))
sk.listen()
conn, addr = sk.accept()

while True:
    ret = conn.recv(1024).decode('utf-8')
    print(ret)
    if ret == '再见':
        conn.send(bytes('再见', encoding='utf-8'))
        break
    info = input('请输入要向客户端发送的内容:')
    conn.send(bytes(info, encoding='utf-8'))

conn.close()
sk.close()

# client.py 客户端

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 8080))

while True:
    info = input('请输入要向服务端发送的内容:')
    sk.send(bytes(info, encoding='utf-8'))
    ret = sk.recv(1024).decode('utf-8')
    print(ret)
    if ret == '再见':
        sk.send(bytes('再见', encoding='utf-8'))
        break

sk.close()

  • 如果客户端1向服务端发送了一条消息,然后客户端2也向服务端发送了一条消息,那么服务端只会收到客户端1的消息并且与客户端1进行通信,只有当客户端1结束了通信服务端才能收到客户端2的消息并且与客户端2进行通讯,以此类推,因为当服务端与客户端进行了链接通信,如果另一台客户端也想与服务端链接通信,那么要将之前的客户端结束通讯后才能与服务端进行链接通信 -> 通俗理解不支持多个客户端和服务端进行同时通讯

# server.py -> 代码没有进行优化,只是为了方便理解

import socket

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))
sk.listen()

# ————————————— 第一台客户端执行的通信代码 ———————————————

conn, addr = sk.accept()

while True:
    ret = conn.recv(1024).decode('utf-8')
    print(ret)
    if ret == '再见':
        conn.send(bytes('再见', encoding='utf-8'))
        break
    info = input('请输入要向客户端发送的内容:')
    conn.send(bytes(info, encoding='utf-8'))

conn.close()

# ————————————— 第二台客户端执行的通信代码 -> 和上面代码一样 ———————————————

conn, addr = sk.accept()

while True:
    ret = conn.recv(1024).decode('utf-8')
    print(ret)
    if ret == '再见':
        conn.send(bytes('再见', encoding='utf-8'))
        break
    info = input('请输入要向客户端发送的内容:')
    conn.send(bytes(info, encoding='utf-8'))

conn.close()

sk.close()  # 如果服务端socket没有关闭,客户端就可以与服务端继续建立链接(通俗理解:就是可以继续执行 conn, addr = sk.accept() 代码)

# server.py -> 优化后代码

import socket

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))
sk.listen()

# ————————————————————————————

while True:
    conn, addr = sk.accept()

    while True:
        ret = conn.recv(1024).decode('utf-8')
        print(ret)
        if ret == '再见':
            conn.send(bytes('再见', encoding='utf-8'))
            break
        info = input('请输入要向客户端发送的内容:')
        conn.send(bytes(info, encoding='utf-8'))

    conn.close()

sk.close()  # 如果服务端socket没有关闭,客户端就可以与服务端继续建立链接(通俗理解:就是可以继续执行 conn, addr = sk.accept() 代码)

# client1.py 客户端1

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 8080))

while True:
    info = input('请输入要向服务端发送的内容:')
    sk.send(bytes('客户端1:' + info, encoding='utf-8'))
    ret = sk.recv(1024).decode('utf-8')
    print(ret)
    if ret == '再见':
        sk.send(bytes('再见', encoding='utf-8'))
        break

sk.close()

# client2.py 客户端2

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 8080))

while True:
    info = input('请输入要向服务端发送的内容:')
    sk.send(bytes('客户端2:' + info, encoding='utf-8'))
    ret = sk.recv(1024).decode('utf-8')
    print(ret)
    if ret == '再见':
        sk.send(bytes('再见', encoding='utf-8'))
        break

sk.close()

4.基于UDP协议的socket


  • 先启动服务端文件再启动客户端文件

  • 在使用 sk.sendto('byte类型的消息',('接收消息方的ip',接收消息方的端口)) 一定要传地址元组,一般地址元组都是使用从 recvfrom 返回的地址,或者客户端第一次向服务端发送消息时自己写的服务端的地址元组

  • 有收必有发,收发必相等 -> 如果客户端执行了 sendto 服务端必须也执行 recvfrom 反之,因为如果客户端或服务端的 sk.recvfrom(1024) 没有接收到消息就会进行阻塞(阻塞: 一直等待,无法运行下面的代码,直到接收到消息)

  • recvfrom(num) 只会接受num个字节内的内容,但是num最好不要超过4096

  • 支持多个客户端和服务端进行同时通讯

  • 在 Pycharm 下切换文件查看打印内容

# server.py 服务端

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)

sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

sk.bind(('127.0.0.1', 8080))

# ————————————————————————————————————

msg, addr = sk.recvfrom(1024)  # 接收客户端发送过来的消息 返回值: 元组 ('bytes类型的消息', 发送消息方的地址元组) -> (b'\xe5\xae\……', ('127.0.0.1', 59608))

print(addr)
print(msg.decode('utf-8'))

sk.sendto(bytes('服务端发送给客户端的消息', encoding='utf-8'), addr)  # 向客户端发送消息,参数: bytes类型的消息, 接收消息方的地址元组 -> 一般使用 sk.recvfrom(1024) 的返回值 addr 就可以,因为 addr 就是接收消息方的地址元组

msg, add = sk.recvfrom(1024)  # 接收客户端发送过来的消息 返回值: 元组 ('bytes类型的消息', 发送消息方的地址元组) -> (b'\xe5\xae\……', ('127.0.0.1', 59608))

print(addr)
print(msg.decode('utf-8'))

sk.close()

# client.py 客户端

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)

ip_port = ('127.0.0.1', 8080)

# ————————————————————————————————————

sk.sendto(bytes('客户端发送给服务端的消息1', encoding='utf-8'), ip_port)  # 向服务端发送消息,参数: bytes类型的消息, 接收消息方的地址元组 -> 第一次发送一般都是向服务端发送,所以接收消息方的地址元组都要自己写的

msg, addr = sk.recvfrom(1024)  # 接收客户端发送过来的消息 返回值: 元组 ('bytes类型的消息',发送方消息方的地址元组) -> (b'\xe5\xae\……', ('127.0.0.1', 8080))

print(addr)
print(msg.decode('utf-8'))

sk.sendto(bytes('客户端发送给服务端的消息2', encoding='utf-8'), addr)  # 向服务端发送消息,参数: bytes类型的消息, 接收消息方的地址元组 -> 这一次的地址元组可以使用 sk.recvfrom(1024) 的返回值 addr

sk.close()

  • 仿qq通信

# server.py 服务端

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))

while True:
    msg, addr = sk.recvfrom(1024)
    print(addr)
    print(msg.decode('utf-8'))
    info = input('>>>')
    sk.sendto(bytes(info, encoding='utf-8'), addr)

sk.close()

# client1.py 客户端1

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = ('127.0.0.1', 8080)

while True:
    info = input('客户端1:')
    sk.sendto(bytes('\033[34m来自客户端1的消息:%s\33[0m' % info, encoding='utf-8'), ip_port)
    msg, addr = sk.recvfrom(1024)
    print(addr)
    print(msg.decode('utf-8'))

sk.close()

# client2.py 客户端2

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = ('127.0.0.1', 8080)

while True:
    info = input('客户端2:')
    sk.sendto(bytes('\033[32m来自客户端2的消息:%s\33[0m' % info, encoding='utf-8'), ip_port)
    msg, addr = sk.recvfrom(1024)
    print(addr)
    print(msg.decode('utf-8'))

sk.close()

  • 服务端接收从客户端发送过来的时间格式,然后服务端按照时间格式发送一个时间个客户端

# server.py 服务端

import socket
import time

sk = socket.socket(type=socket.SOCK_DGRAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))
while True:
    time_format, addr = sk.recvfrom(1024)
    t = time_format.decode('utf-8')
    sk.sendto(bytes(time.strftime(t), encoding='utf-8'), addr)

sk.close()

# client.py 客户端

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = ('127.0.0.1', 8080)
while True:
    time_format = input('请输入时间格式:')
    sk.sendto(time_format.encode('utf-8'), ip_port)
    t, addr = sk.recvfrom(1024)
    print(t.decode('utf-8'))

sk.close()

5.如果客户端断开了链接且服务端还在继续接收客户端的信息,那么接收到的是空信息,不会报错的

# server.py

import socket

sk = socket.socket()
sk.bind(('127.0.0.1', 8080))
sk.listen()
conn, addr = sk.accept()

while True:
    msg = conn.recv(1024).decode('utf-8')
    print(msg)

# client.py

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 8080))

sk.send(b'hello')

sk.close()

6.黏包

  • 只有TCP协议有黏包现象,UDP协议永远不会黏包,但是UDP协议过消息或数据过长,那么他会丢弃过长的那部分或报错

  • 黏包现象:
  1. 如果发送的消息过大接收端得到的结果很有可能是一部分,接收端再执行接收消息的时候又会先接收到之前剩余的部分再接收新消息 -> 客户端向服务端发送一段很大的数据,如果接收端的recv(num)中的num的值小于发送过来的数据的字节数的话,那么接收端就会从缓存区中取只在num个字节内的值,如果服务端再次接受数据的时候就会从缓存取中先获取上次没有接收完的数据,然后再取新数据
  2. 同时执行 sk.send(b'hello') 和 sk.send(b'world') 所收到的结果是 helloworld,因为两次发送的消息过小且时间间隔短,那么就会将两次消息合并在一起发送

  • 黏包成因:
    • 黏包现象1的解释:因为TCP协议中有拆包机制 -> 如果本机的MTU(网络发送的最大数据包)大于网关的MTU,那么大的数据包最会被拆开来传递 -> 通俗理解: 如果发送的消息或数据过大TCP就会拆开传递
    • 黏包现象2的解释:因为在TCP协议中使用了优化算法(Nagle算法),它会将多次时间间隔较小且数据量小的数据,合并成一个大的数据块来进行传递


    • recv(num)一旦被调用,就会尝试获取缓冲区中的数据,只要有数据,就会直接返回,如果 num 为1024那么就会从缓存区中取1024字节的数据,如果缓存区只有400字节的数据,那么也只会取400字节的数据,而多出来的数据只能在下次 recv(num) 是获取
    • 通俗理解: 同时执行多个 send() 的时候,且 send() 之间没有执行recv() 就会有可能出现黏包现象
    • 总结: 主要还是因为接收端不知道一次要 recv(num) 多少个字节所构成的,如果每次执行 recv(num) 的时候都知道准确的消息字节数的话就不会发生黏包问题了,不连续 send() 的,且send()的后面跟着 recv(num) 也不会出现黏包问题

  • TCP协议的黏包现象1:

# server.py 服务端

import socket

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))
sk.listen()
conn, addr = sk.accept()

msg = conn.recv(2)  # 限制了只获取2个字节的内容
print(msg.decode('utf-8'))  # he 因为消息过长只会获取到一部分
msg = conn.recv(10)  # 限制了只获取10个字节的内容
print(msg.decode('utf-8'))  # llo-Kevin 获取上次没有获取到的部分

conn.close()
sk.close()

# client.py 客户端

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 8080))

sk.send(bytes('hello-Kevin', encoding='utf-8'))

sk.close()

  • TCP协议的黏包现象2:

# server.py 服务端

import socket

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))
sk.listen()
conn, addr = sk.accept()

msg1 = conn.recv(1024)
print(msg1)  # b'helloworld'
msg1 = conn.recv(1024)
print(msg1)  # b'' -> 当发送端关闭 socket 的时候就会发送一个空的值过来
msg1 = conn.recv(1024)
print(msg1)  # b''

conn.close()
sk.close()

# client.py 客户端

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 8080))

sk.send(b'hello')
sk.send(b'world')

sk.close()

  • 基于TCP实现黏包例子: 远程执行命令 -> 在server端发送命令行命令给client端执行并且把执行结果返回个服务端 -> subprocess模块,执行系统命令会分别返回执行成功和失败的结果

# server.py

import socket

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))
sk.listen()
conn, addr = sk.accept()

while True:
    cmd = input('请输入正确或者错误的系统命令:')
    conn.send(cmd.encode('utf-8'))
    ret = conn.recv(1024).decode('utf-8')
    print(ret)
conn.close()
sk.close()

# client.py

import socket
import subprocess

sk = socket.socket()
sk.connect(('127.0.0.1', 8080))
while True:
    cmd = sk.recv(1024).decode('utf-8')
    ret = subprocess.Popen(
        cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    stdout = '执行成功的返回值:' + (ret.stdout.read()).decode('gbk')
    stderr = '执行失败的返回值:' + (ret.stderr.read()).decode('gbk')
    print(stdout)
    print(stderr)
    sk.send(stdout.encode('utf-8'))
    sk.send(stderr.encode('utf-8'))

sk.close()

  • UDP协议永远不会黏包,但是UDP协议过消息或数据过长,那么他会丢弃过长的那部分,如果接受到的消息字节数大于 recvfrom() 所设置的数就会报错

    • 基于UDP实现丢弃过长部分: 远程执行命令 -> 在server端发送命令行命令给client端执行并且把执行结果返回个服务端 -> subprocess模块,执行系统命令会分别返回执行成功和失败的结果

# server.py

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))
msg, addr = sk.recvfrom(1024)

while True:
    cmd = input('请输入正确或者错误的系统命令:')  # 输入循序 ls -> ipconfig -> dir
    sk.sendto(cmd.encode('utf-8'), addr)
    msg, addr = sk.recvfrom(10240)
    print(msg.decode('utf-8'))

sk.close()

# client.py

import socket
import subprocess

sk = socket.socket(type=socket.SOCK_DGRAM)
ip_sort = ('127.0.0.1', 8080)
sk.sendto(bytes('为了服务端能获取到客户端的地址才发此条消息', encoding='utf-8'), ip_sort)

while True:
    cmd, addr = sk.recvfrom(1024)
    ret = subprocess.Popen(
        cmd.decode('utf-8'),
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    stdout = '执行成功的返回值:' + (ret.stdout.read()).decode('gbk')
    stderr = '执行失败的返回值:' + (ret.stderr.read()).decode('gbk')
    print(stdout)
    print(stderr)
    sk.sendto(stdout.encode('utf-8'), addr)
    sk.sendto(stderr.encode('utf-8'), addr)

sk.close()

    • 报错现象 -> 解决方法:接收端的 recvfrom 的值设置大于接收到消息的字节数

# server.py 服务端

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))

msg, addr = sk.recvfrom(1024)
print(msg.decode('utf-8'))  # 报错

msg, addr = sk.recvfrom(1024)
print(msg.decode('utf-8'))

# client.py 客户端

import socket

s1 = r''' 驱动器 C 中的卷没有标签。
卷的序列号是 000B-A3DF

C:\Users\KX201711 的目录

2019/02/12  08:27    <DIR>          .
2019/02/12  08:27    <DIR>          ..
2019/01/23  16:58    <DIR>          .android
2018/10/13  14:55    <DIR>          .AndroidStudio2.3
2018/10/15  10:43    <DIR>          .AndroidStudio3.2
2018/06/22  16:00    <DIR>          .atom
2018/10/22  16:46         7,430,209 .babel.json
2019/01/14  15:13             8,979 .bash_history
2019/02/15  08:34    <DIR>          .BigNox
2017/11/13  08:24    <DIR>          .config
2018/08/03  16:59                37 .dbshell
2018/10/16  09:52    <DIR>          .electron
2018/10/15  15:50                16 .emulator_console_auth_token
2018/10/15  14:44    <DIR>          .expo
2017/11/18  13:41    <DIR>          .gem
2017/11/18  14:16               121 .gemrc
2018/08/20  08:31               174 .gitconfig
2018/10/13  16:44    <DIR>          .gradle
2018/07/10  08:12                42 .minttyrc
2018/08/03  09:25                 0 .mongorc.js
2018/03/17  10:33             9,350 .v8flags.6.2.414.50.dba84d59341dcb557c327ad510bd5ce2.json
2019/01/07  13:56            11,876 .viminfo
2018/01/19  16:00    <DIR>          新建文件夹
              21 个文件    846,679,223 字节
              50 个目录 28,960,673,792 可用字节'''

s2 = r'''
Windows IP 配置


以太网适配器 以太网:

   连接特定的 DNS 后缀 . . . . . . . : www.tendawifi.com
   本地链接 IPv6 地址. . . . . . . . : fe80::295f:b971:88c8:f39d%11
   IPv4 地址 . . . . . . . . . . . . : 192.168.0.138
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 192.168.0.1

以太网适配器 VirtualBox Host-Only Network:

   连接特定的 DNS 后缀 . . . . . . . :
   本地链接 IPv6 地址. . . . . . . . : fe80::d10d:58d8:d36e:107f%6
   IPv4 地址 . . . . . . . . . . . . : 192.168.56.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . :

以太网适配器 VirtualBox Host-Only Network #2:

   连接特定的 DNS 后缀 . . . . . . . :
   本地链接 IPv6 地址. . . . . . . . : fe80::e8:372e:bf52:6d95%5
   IPv4 地址 . . . . . . . . . . . . : 192.168.224.2
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . :'''

sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = ('127.0.0.1', 8080)
sk.sendto(bytes(s1, encoding='utf-8'), ip_port)
sk.sendto(bytes(s2, encoding='utf-8'), ip_port)

7.黏包的解决方案一

  • 问题的根源在于,接收端不知道发送端将要传送数据的字节长度,所以解决黏包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节大小让接收端知晓,然后接收端通过总字节数限制一次接受多少数据

  • 注意要以bytes类型去计算所要发送消息的字节长度,因为 recv 是按字节长度去获取的


  • 使用这种方式所存在的问题就是多了一次 send 和 recv 的交互

# 解决远程执行命令例子中的黏包问题

# server.py

import socket

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8081))
sk.listen()
conn, addr = sk.accept()

while True:
    cmd = input('请输入正确或者错误的系统命令:')
    conn.send(cmd.encode('utf-8'))
    num = conn.recv(1024).decode('utf-8')  # 接收内容的总字节数
    conn.send(b'ok')  # 发送确认消息
    ret = conn.recv(int(num)).decode('gbk')  # 通过总字节数设置可以接受多少数据,比如接收到的消息的总字节数为5,那么 recv 只会接收5个字节数的消息,这样就不会发生黏包问题
    print(ret)

conn.close()
sk.close()

# client.py

import socket
import subprocess

sk = socket.socket()
sk.connect(('127.0.0.1', 8081))

while True:
    cmd = sk.recv(1024).decode('utf-8')
    ret = subprocess.Popen(
        cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    stdout = ret.stdout.read()
    stderr = ret.stderr.read()
    sk.send(str(len(stdout) + len(stderr)).encode('utf-8'))  # 将内容的总字节数首先发送给服务端 -> 注意要以bytes类型去计算所要发送消息的字节长度,因为 recv 是按字节长度去获取的
    sk.recv(1024)  # 接收确认消息,如果没有接收此消息,那么在这里就会发生黏包问题
    sk.send(stdout)
    sk.send(stderr)

sk.close()

8.黏包的解决方案二

  • 使用struct模块将要发送的内容的总字节数转换为固定的bytes类型传给接收端,然后接受端以固定的字节数接收该bytes类型,这样就算发送端连续执行send()发送数据出现了黏包问题,客户端也可以通过固定的字节数接收到该数据,这样也解决了黏包解决方案一中多了一次 send 和 recv 的交互的问题

# 解决远程执行命令例子中的黏包问题

# server.py

import socket
import struct

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8081))
sk.listen()
conn, addr = sk.accept()

while True:
    cmd = input('请输入正确或者错误的系统命令:')
    conn.send(cmd.encode('utf-8'))
    num = conn.recv(4)  # b'A\x00\x00\x00' 接收固定长度的bytes类型的内容总字节数 -> 以4个字节去读取内容总字节数,因为int类型通过struct模块转化后得到的bytes类型的长度是4
    num = struct.unpack('i', num)[0]  # (65,)[0] 对固定长度的bytes类型进行解码,获取到内容总字节数
    ret = conn.recv(int(num)).decode('gbk')  # 通过总字节数设置可以接受多少数据,比如接收到的消息的总字节数为5,那么 recv 只会接收5个字节数的消息,这样就不会发生黏包问题
    print(ret)

conn.close()
sk.close()

# client.py

import socket
import subprocess
import struct

sk = socket.socket()
sk.connect(('127.0.0.1', 8081))

while True:
    cmd = sk.recv(1024).decode('utf-8')
    ret = subprocess.Popen(
        cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    stdout = ret.stdout.read()
    stderr = ret.stderr.read()
    len_num = len(stdout) + len(stderr)
    bytes_num = struct.pack('i', len_num)  # b'A\x00\x00\x00' 将内容总字节数转化为固定长度的bytes类型
    sk.send(bytes_num)  # 发送固定长度的bytes类型给服务端
    sk.send(stdout)
    sk.send(stderr)

sk.close()

9. 报头和报文

  • 在网络上传输的所有数据都叫数据包
  • 数据包里面的所有数据都叫报文
  • 所有的报文都会有报头 / 所有的协议都会有报头
  • 报头,在数据的传输过程中不止只有你的数据,还有ip地址 端口 mac地址……这些就是报头 -> 通俗理解: 除了你要传输的数据之外的消息就叫报头
  • 报头的作用: 比如客户端要向服务端发送一个文件且服务端想知道该文件的相关信息,例如:文件的名字/大小/类型,这时候我们就可以定制一个报文发送个服务端 -> 上面提到的解决黏包问题的总字节数一般都会放在报头(字典)中发送给接收端解决黏包问题
  • 报头也可以理解为发送端最先发送的关于该数据的相关信息的消息,一般都是字典 -> 上面解决黏包方法中先发送一个总字节数的固定长度的bytes类型给服务端,而当中的固定长度的bytes类型就是报头
  • 报头是最先发送的也是接收端最先接收到的

# server.py

import socket
import struct
import json

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8081))
sk.listen()
conn, addr = sk.accept()

h_fixed_len_bytes = conn.recv(4)  # 接收固定长度的bytes类型的报头总字节数
h_fixed_len = struct.unpack('i', h_fixed_len_bytes)  # (76,) 对固定长度的bytes类型进行解码,获取到报头总字节数
h_json = conn.recv(h_fixed_len[0]).decode('utf-8')  # 通过总字节数设置可以接受多少数据从而获取到字符串类型的报头
header = json.loads(h_json)  # 将字符串类型的报头装换为Python代码
print(header)  # {'msg_len': 24, 'msg_info': '关于msg的相关消息'}
msg_len = int(header['msg_len'])  # 通过报头获取到要接收的信息的总字节数
msg = conn.recv(msg_len).decode('utf-8')  # 通过总字节数获取到客户端发送过来的消息
print(msg)  # 服务端接收的消息

sk.close()

# client.py -> 将要发送的内容的总字节数放在报头中发送给服务端,服务端通过报头中的总字节数接收相关字节数长度的内容

import socket
import struct
import json

sk = socket.socket()
sk.connect(('127.0.0.1', 8081))

msg = bytes('服务端接收的消息', encoding='utf-8')  # 需要发送给服务端的数据 -> 报文

header = {  # 定制报头
    'msg_len': len(msg),  # 需要发送给服务端的数据的总字节数
    'msg_info': '关于msg的相关消息'
}
h_json = json.dumps(header)  # 将报头转换为json字符串 -> {"msg_info": "\u5173\……", "msg_len": 24}
h_bytes = bytes(h_json, encoding='utf-8')  # 将json字符串转换为bytes类型 -> b'{"msg_info": "\\u5173\\……, "msg_len": 24}'
h_bytes_len = len(h_bytes)  # 得到报头的总字节数 -> 76
h_fixed_len_bytes = struct.pack('i', h_bytes_len)  # 将报头的总字节数转化为固定长度的bytes类型 -> b'L\x00\x00\x00'

sk.send(h_fixed_len_bytes)  # 发送固定长度的bytes类型的报头总字节数
sk.send(bytes(h_json, encoding='utf-8'))  # 发送报头
sk.send(msg)  # 发送报文

sk.close()

10. 实现大文件的上传或下载 -> 大文件的传输

file_transfer.rar

# server.py -> 接收端

import socket
import struct
import json

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8081))
sk.listen()
conn, addr = sk.accept()
buffer = 1024  # 每次写入多少字节的数据,这里最好写1024因为数值越大就越容易出错

bytes_header_len = conn.recv(4)  # 接收固定长度的bytes类型的报头总字节数
header_len = struct.unpack('i', bytes_header_len)  # 对固定长度的bytes类型进行解码,获取到报头总字节数
json_header = conn.recv(header_len[0]).decode('utf-8')  # 通过总字节数设置可以接受多少数据从而获取到字符串类型的报头
header = json.loads(json_header)  # 将字符串类型的报头装换为Python代码

filesize = header['filesize']  # 从报头中获取文件的大小(总字节数)
with open(r'movie\%s' % header['filename'], 'wb') as f:  # 以bytes类型的方式写入文件,因为传送过来的数据是bytes类型的 -> 根目录下一定要有movie文件夹
    while filesize:
        if filesize >= buffer:
            print(filesize, 1)
            content = conn.recv(buffer)  # 按照固定的字节去接收数据
            f.write(content)  # 将数据写入到文件中
            filesize -= buffer  # 因为上面已经接收了1024个字节,所以 总节数-buffer = 剩下的未接收的字节数
        else:
            print(filesize, 2)
            content = conn.recv(filesize)  # 接收最后剩下字节数且该字节数已经小于buffer的数据
            f.write(content)  # 将数据写入到文件中
            filesize = 0
            print(filesize, 3)

conn.close()
sk.close()

# client.py -> 发送端

import socket
import struct
import json
import os

sk = socket.socket()
sk.connect(('127.0.0.1', 8081))
buffer = 1024  # 每次发送多少字节的数据,这里最好写1024因为数值越大就越容易出错

header = {  # 定制报文 -> 要发送文件的相关信息
    'filepath': r'D:\Movie',
    'filename': r'Ghost blowing light.mp4'
}
file_path = os.path.join(header['filepath'], header['filename'])  # 拼接文件夹名和文件名从而获得文件的完整路径
filesize = os.path.getsize(file_path)  # 获取文件大小(总字节数)
header['filesize'] = filesize  # 将文件大小(总字节数)添加到报头里
json_header = json.dumps(header)  # 将报头转为字符串
bytes_header = bytes(json_header, encoding='utf-8')  # 将报头转化为bytes类型
header_len = len(bytes_header)  # 获取报头的总字节数
bytes_header_len = struct.pack('i', header_len)  # 将报头的总字节数转化为固定长度的bytes类型
sk.send(bytes_header_len)  # 发送固定长度的bytes类型的报头总字节数
sk.send(bytes_header)  # 发送报头

with open(file_path, 'rb') as f:  # bytes类型读取文件,因为 sk.send() 就是发送bytes类型的
    while filesize:
        if filesize >= buffer:
            print(filesize, 1)
            content = f.read(buffer)  # 按照固定的字节去读取文件
            sk.send(content)  # 将读取到的文件发送个服务端
            filesize -= buffer  # 因为上面已经读取了1024个字节,所以 总节数-buffer = 剩下的未读的字节数
        else:
            print(filesize, 2)
            content = f.read(filesize)  # 读取最后剩下字节数且该字节数已经小于buffer的内容
            sk.send(content)  # 将读取到的文件发送个服务端
            filesize = 0
            print(filesize, 3)

sk.close()


11. 实现大文件的上传或下载后验证文件的一致性(验证两个文件是否一致)

file_consistency.rar

# server.py

import socket
import struct
import json
import hashlib

md5 = hashlib.md5()

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8081))
sk.listen()
conn, addr = sk.accept()
buffer = 1024  # 每次写入多少字节的数据,这里最好写1024因为数值越大就越容易出错

bytes_header_len = conn.recv(4)  # 接收固定长度的bytes类型的报头总字节数
header_len = struct.unpack('i', bytes_header_len)  # 对固定长度的bytes类型进行解码,获取到报头总字节数
json_header = conn.recv(header_len[0]).decode('utf-8')  # 通过总字节数设置可以接受多少数据从而获取到字符串类型的报头
header = json.loads(json_header)  # 将字符串类型的报头装换为Python代码

filesize = header['filesize']  # 从报头中获取文件的大小(总字节数)
with open(r'movie\%s' % header['filename'], 'wb') as f:  # 以bytes类型的方式写入文件,因为传送过来的数据是bytes类型的 -> 根目录下一定要有movie文件夹
    while filesize:
        if filesize >= buffer:
            print(filesize, 1)
            content = conn.recv(buffer)  # 按照固定的字节去接收数据
            md5.update(content)  # 使用多次 update 获取 md5 值
            f.write(content)  # 将数据写入到文件中
            filesize -= buffer  # 因为上面已经接收了1024个字节,所以 总节数-buffer = 剩下的未接收的字节数
        else:
            print(filesize, 2)
            content = conn.recv(filesize)  # 接收最后剩下字节数且该字节数已经小于buffer的数据
            md5.update(content)  # 使用多次 update 获取 md5 值
            f.write(content)  # 将数据写入到文件中
            filesize = 0
            print(filesize, 3)

ciphertext = md5.hexdigest()  # 得到上传完后文件的md5值
print(ciphertext, 1)

client_ciphertext = conn.recv(1024).decode('utf-8')  # 接收客户端发送过来的原文件的md5值
print(client_ciphertext, 2)

if ciphertext == client_ciphertext:  # 使用 md5 进行文件的一致性验证
    print('上传完后的文件和原文件一致')

conn.close()
sk.close()

# client.py

import socket
import struct
import json
import os
import hashlib

md5 = hashlib.md5()

sk = socket.socket()
sk.connect(('127.0.0.1', 8081))
buffer = 1024  # 每次发送多少字节的数据,这里最好写1024因为数值越大就越容易出错

header = {  # 定制报文 -> 要发送文件的相关信息
    'filepath': r'D:\Movie',
    'filename': r'Ghost blowing light.mp4'
}
file_path = os.path.join(header['filepath'], header['filename'])  # 拼接文件夹名和文件名从而获得文件的完整路径
filesize = os.path.getsize(file_path)  # 获取文件大小(总字节数)
header['filesize'] = filesize  # 将文件大小(总字节数)添加到报头里
json_header = json.dumps(header)  # 将报头转为字符串
bytes_header = bytes(json_header, encoding='utf-8')  # 将报头转化为bytes类型
header_len = len(bytes_header)  # 获取报头的总字节数
bytes_header_len = struct.pack('i', header_len)  # 将报头的总字节数转化为固定长度的bytes类型
sk.send(bytes_header_len)  # 发送固定长度的bytes类型的报头总字节数
sk.send(bytes_header)  # 发送报头

with open(file_path, 'rb') as f:  # bytes类型读取文件,因为 sk.send() 就是发送bytes类型的
    while filesize:
        if filesize >= buffer:
            print(filesize, 1)
            content = f.read(buffer)  # 按照固定的字节去读取文件
            md5.update(content)  # 使用多次 update 获取 md5 值
            sk.send(content)  # 将读取到的文件发送个服务端
            filesize -= buffer  # 因为上面已经读取了1024个字节,所以 总节数-buffer = 剩下的未读的字节数
        else:
            print(filesize, 2)
            content = f.read(filesize)  # 读取最后剩下字节数且该字节数已经小于buffer的内容
            md5.update(content)  # 使用多次 update 获取 md5 值
            sk.send(content)  # 将读取到的文件发送个服务端
            filesize = 0
            print(filesize, 3)

ciphertext = md5.hexdigest()  # 得到原文件 md5 值
print(ciphertext)
sk.send(bytes(ciphertext, encoding='utf-8'))  # 将原文件 md5 值发送给服务端进行文件的一致性验证

sk.close()

12. 验证客户端的合法性

  • 当服务端不想随便被任何一个客户端进行链接,只能通过密钥和服务端发送过来的消息进行加密,然后将加密后的内容发送个服务端进行验证
  • 使用 hmac加密模块进行加密,也可以使用hashlib加密模块

# server.py

import os
import hmac
import socket

key = '123'  # 密钥/盐

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))
sk.listen()

def check_conn(conn):
    msg = os.urandom(32)  # 随机生成长度为32位的bytes类型
    conn.send(msg)  # 将随机生成长度为32位的bytes类型发送个客户端,进行和密钥的加密
    h = hmac.new(bytes(key, encoding='utf-8'), msg, digestmod='MD5')  # 将密钥和随机生成长度为32位的bytes类型进行md5加密
    digest = h.digest()  # 获取加密后的值,以二进制的形式返回,且是bytes类型
    client_digest = conn.recv(1024)  # 接收客户端通过密钥加密后的信息
    return hmac.compare_digest(digest, client_digest)  # 比较服务端和客户端加密后的信息是否一致

conn, addr = sk.accept()
res = check_conn(conn)

if res:
    print('合法客户端')
else:
    print('不合法客户端')

# client.py

import os
import hmac
import socket

key = '123'  # 密钥/盐

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.connect(('127.0.0.1', 8080))

msg = sk.recv(1024)  # 接收服务端随机生成长度为32位的bytes类型
h = hmac.new(bytes(key, encoding='utf-8'), msg, digestmod='MD5')  # 将密钥和服务端发送过来的消息进行加密
digest = h.digest()  # 获取加密后的值,以二进制的形式返回,且是bytes类型
sk.send(digest)  # 将加密后的值发送给服务端进行验证,判断该客户端是否合法

sk.close()

13. socket的其他方法

  • sk.sendall() -> 将所有数据一次性发送过去,有可能会会出现丢包 -> 还是建议使用 send

    • sendall 和 send 的区别就是 send 如果要发送的数据过大就会将数据拆开发送(简称拆包),而sendall则不会

# server.py

import socket

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))
sk.listen()
conn, addr = sk.accept()

msg = conn.recv(1024).decode('utf-8')
print(msg)

conn.close()
sk.close()

# client.py

import socket

sk = socket.socket()
sk.connect(('127.0.0.1', 8080))

sk.sendall(bytes('先服务端发送的数据', encoding='utf-8'))

sk.close()

  • sk.setblocking(True/False) ->  设置socket的阻塞和非阻塞模式

    • 非阻塞模式就是遇见 listen 或 recv 都不进行阻塞直接往下执行

    • True: 阻塞 False: 非阻塞 默认值: True 
    • 将 socket 设置为非阻塞模式的时候,所有本来阻塞的程序都会变成非阻塞(如 sk.accept conn.recv),当程序执行的时候就会报错

# server.py -> 当 server 被启动的时候就会报错

import socket

sk = socket.socket()
sk.setblocking(False)  # 设置 socket 的阻塞和非阻塞模式
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(('127.0.0.1', 8080))
sk.listen()
conn, addr = sk.accept()

msg = conn.recv(1024).decode('utf-8')
print(msg)

conn.close()
sk.close()